﻿/*
Ref mapping ... [mscorlib..]
Ref mapping ... [system..]

    private MappedPackage GetMapped(string mapping, AssemblyName assembly_name)
    {
      MappedPackage mapped = PackageMap.Instance.GetMapped(mapping);
      mapped.AssemblyNames.Add(assembly_name); <- here add up the ref count
      return mapped;
    }
*/
using System;
using System.Linq;
using System.Collections.Generic;
using DependencyLib;
using CrossFlow.GraphLib;
using System.IO;
using System.Reflection;

namespace Dependency
{
  class Input
  {
    public bool Tree { get; set; }
    public bool Confirm { get; set; }
    public string Log { get; set; }
    public string Label { get; set; }
    public string Visio { get; set; }
    public string Excel { get; set; }
    public string DGML { get; set; }
    public HashSet<string> FileInclude { get; set; }
    public HashSet<string> FileExclude { get; set; }
    public HashSet<string> TypeNameExclude { get; set; } //TODO: This is for -when- type-level dependencies
    public DirectoryInfo[] Folders { get; set; }
    public HashSet<string> FolderExclude { get; set; }
    public Dictionary<string, List<string>> Mappings { get; set; }

    internal void Validate()
    {
      var now = DateTime.Now;
      this.Visio = string.IsNullOrWhiteSpace(this.Visio) ? default_name("vdx", now) : default_type_name("vdx",this.Visio, now);
      this.Excel = string.IsNullOrWhiteSpace(this.Excel) ? default_name("csv", now) : default_type_name("csv", this.Excel, now);
      this.Log = string.IsNullOrWhiteSpace(this.Log) ? default_name("log", now) : default_type_name("log", this.Log, now);
      this.DGML = string.IsNullOrWhiteSpace(this.DGML) ? default_name("dgml", now) : default_type_name("dgml", this.DGML, now);
      if (this.FileInclude == null || this.FileInclude.Count == 0)
      {
        this.FileInclude = new HashSet<string>();
        this.FileInclude.Add("*.dll");
        this.FileInclude.Add("*.exe");
      }
      if (this.FolderExclude == null || this.FolderExclude.Count == 0)
      {
        this.FolderExclude = new HashSet<string>();
        this.FolderExclude.Add("obj");
      }
    }

    private string default_name(string file_extension, DateTime now)
    {
      return default_type_name(file_extension, string.Format("DAG{0}-{1}", string.IsNullOrEmpty(this.Label) ? null : "-" + this.Label, now.ToString("yyyyMMdd-hhmmss-fff")), now);
    }

    private string default_type_name(string file_extension, string nameFromInput, DateTime now)
    {
      string name = Path.GetFileNameWithoutExtension(nameFromInput);
      return string.IsNullOrEmpty(name) ? default_name(file_extension, now) : string.Format("{0}.{1}", name, file_extension);
    }

    internal void Show(string[] args)
    {
      System.Diagnostics.Trace.Write("Command-line:");
      foreach(string arg in args)
        if(arg.Contains(' '))
          System.Diagnostics.Trace.Write(string.Format(" \"{0}\"",arg));
        else
          System.Diagnostics.Trace.Write(string.Format(" {0}", arg));
      System.Diagnostics.Trace.WriteLine(Environment.NewLine);
      System.Diagnostics.Trace.WriteLine(string.Format("Label:\t{0}", this.Label));
      System.Diagnostics.Trace.WriteLine(string.Format("Log:\t{0}", this.Log));
      System.Diagnostics.Trace.WriteLine(string.Format("Visio:\t{0}", this.Visio));
      System.Diagnostics.Trace.WriteLine(string.Format("Excel:\t{0}", this.Excel));
      System.Diagnostics.Trace.WriteLine("File Include:");
      foreach (string f in this.FileInclude)
        System.Diagnostics.Trace.WriteLine(string.Format("\t{0}", f));
      System.Diagnostics.Trace.WriteLine("File Exclude:");
      if (this.FileExclude != null)
        foreach (string f in this.FileExclude)
          System.Diagnostics.Trace.WriteLine(string.Format("\t{0}", f));
      System.Diagnostics.Trace.WriteLine(string.Format("Folders{0}:", this.Tree ? " recursively" : string.Empty));
      if (this.Folders != null)
        foreach (var f in this.Folders)
          System.Diagnostics.Trace.WriteLine(string.Format("\t{0}", f.FullName));
      System.Diagnostics.Trace.WriteLine("Folder Excludes:");
      if (this.FolderExclude != null)
        foreach (var f in this.FolderExclude)
          System.Diagnostics.Trace.WriteLine(string.Format("\t{0}", f));
      System.Diagnostics.Trace.WriteLine("Mappings");
      if (this.Mappings != null)
        foreach (string mapping in this.Mappings.Keys)
        {
          System.Diagnostics.Trace.WriteLine(string.Format("\tMapping:{0}", mapping));
          foreach (string mapped in this.Mappings[mapping])
              System.Diagnostics.Trace.WriteLine(string.Format("\t\tMapped:{0}", mapped));
        }
      System.Diagnostics.Trace.WriteLine("");
    }
  }

  class Program
  {
    static System.Threading.ThreadStart usage = delegate
    {
      System.Diagnostics.Trace.WriteLine("\nUsage: Dependency.exe [/tree] [/DGML:<filename>.dgml] [/Visio:<filename>.vdx] [/Excel:<filename>.csv] /Label=<label> [/log=<filename>.log] [/FileInclude <search-pattern> [<search-pattern>]*] [/FileExclude <assembly> [<assembly>]*] [/FolderExclude <name> [<name>]*] [\"/mappings=<<-mapping> <mapped> [<mapped]>]>* \"] /folders <assemblies-root-folder> [<assemblies-root-folder>]*");
      //TODO: /append:<filename>.vdx
      //TODO: typeincludes refers kind of namespaces, not assemblies. -> which I found to be untrue because assembly <-> file
      //TODO: X-Y graph with point labels http://support.microsoft.com/kb/213750
      System.Diagnostics.Trace.WriteLine("\nSample 1:\nDependency.exe /tree /FileInclude *.dll /FileExclude Microsoft Presentation \"/mappings=-DOTNET System mscorlib\" /folders C:\\codebase");
      System.Diagnostics.Trace.WriteLine("\nSample 2:\nDependency.exe /label=X \"/mappings=-Core ABC.Core -NET System mscorlib -ENTLIB Microsoft\" /folders C:\\codebase");
      System.Diagnostics.Trace.WriteLine("\nSample 3:\nDependency.exe @responsefile.txt");
      System.Diagnostics.Trace.WriteLine("\nNote for Powershell users: Problems specifying arguments? Try enclosing them with double or single quotes.\nTry, alternatively, a text response file where each line would be a Windows command-line argument.\n");
    };

    static void prologue()
    {
      System.Diagnostics.Trace.WriteLine(string.Format("\nDependency Command-line Tool {0} part of Toolset for the Software Designer.", '\x2014'));
      System.Diagnostics.Trace.WriteLine(string.Format("Copyright © Textum 2009 - 2014. Marco A. Dorantes. Version {0}\n", typeof(Program).Assembly.GetName().Version));
    }

    const string temporal_listener = "temporal";
    static StringWriter temporal_writer;

    static void Main(string[] args)
    {
      try
      {
        InitialLogSetup();
        prologue();
        if (args.Length == 0)
        {
          usage();
          return;
        }
        Input input = utility.Switch.AsType<Input>(args);
        input.Validate();
        input.Show(args);

        if (input.Folders == null || input.Folders.Length == 0)
        {
          usage();
          return;
        }

        if (input.Confirm)
        {
          Console.WriteLine("\nDenote YES to continue processing...");
          string response = Console.ReadLine();
          if (response.ToLower() != "yes")
            return;
        }
        FinalLogSetup(input.Log);
        DirectedGraph graph = ScanAndBuildWholeDAG(input);
        stability_metrics(graph);
        WritingResults(graph, input);
        System.Diagnostics.Trace.WriteLine("\n*** Dependency.exe completed successfully. ***\n");
      }
      catch (Exception ex)
      {
          System.Diagnostics.Trace.WriteLine(string.Format("{0} :{1}\nStack trace:\n{2}", ex.GetType().FullName, ex.Message, ex.StackTrace));
      }
    }

    static void InitialLogSetup()
    {
      temporal_writer = new StringWriter();
      System.Diagnostics.Trace.Listeners.Add(new System.Diagnostics.TextWriterTraceListener(Console.Out));
      System.Diagnostics.Trace.Listeners.Add(new System.Diagnostics.TextWriterTraceListener(temporal_writer, temporal_listener));
      System.Diagnostics.Trace.AutoFlush = true;
    }

    static void FinalLogSetup(string logfile)
    {
      var loglistener = new System.Diagnostics.TextWriterTraceListener(logfile);
      loglistener.WriteLine(temporal_writer.GetStringBuilder().ToString());
      System.Diagnostics.Trace.Listeners.Add(loglistener);
      System.Diagnostics.Trace.Listeners.Remove(temporal_listener);
    }

    static DirectedGraph ScanAndBuildWholeDAG(Input input)
    {
      System.Diagnostics.Trace.WriteLine("\n*** Scanning for dependencies ***\n");
      DirectedGraph graph = new DirectedGraph("DAG");
      foreach (DirectoryInfo rootfolder in input.Folders)
      {
        System.Diagnostics.Trace.Write(string.Format("\nScanning [{0}]", rootfolder.FullName));
        if (input.Tree)
          System.Diagnostics.Trace.Write(" and subfolders");
        System.Diagnostics.Trace.WriteLine("...");
        if (!Directory.Exists(rootfolder.FullName))
        {
          dir_notfound(rootfolder.FullName);
          continue;
        }
        foreach (string include_filter in input.FileInclude)
        {
          var file_iterator = new AssemblyFolderIterator(rootfolder, include_filter, input.FileExclude, input.FolderExclude, input.Tree);
          file_iterator.OnFileIncluded += file_included;
          file_iterator.OnFileExcluded += file_excluded;
          file_iterator.OnFileNotFound += notfound;
          file_iterator.OnFileNotLoad += notload;
          file_iterator.OnBadImageFormat += badimage;

          var package_iterator = new PackageMapper<Assembly>(file_iterator, input.Mappings);

          using (IDependencyProvider depends = new PackageDependencyProvider(package_iterator, input.FileExclude, input.Mappings))
          {
            depends.OnReferenceIncluded += ref_included;
            depends.OnReferenceExcluded += ref_excluded;
            depends.OnMappedPackage += package_mapped;
            depends.AppendToGraph(graph);
          }
        }
      }
      return graph;
    }

    static void stability_metrics(DirectedGraph graph)
    {
      System.Diagnostics.Trace.WriteLine("\n*** Calculating stability metrics ***\n");
      foreach (Vertex package in graph.Vertexes)
      {
        AssemblyAbstraction A = package.Data as AssemblyAbstraction;
        if (A == null) continue;
        decimal edge_total = (decimal)(package.IncomingEdgeCount + package.OutgoingEdgeCount);
        if (edge_total > 0)
          A.Instability = (decimal)package.OutgoingEdgeCount / edge_total;
      }
      summary(graph);
    }

    static void summary(DirectedGraph graph)
    {
      System.Diagnostics.Trace.WriteLine("\n*** DAG Summary ***\n");
      System.Diagnostics.Trace.WriteLine(string.Format("Node count:\t{0}",graph.NodeCount));
      System.Diagnostics.Trace.WriteLine(string.Format("Vertex count:\t{0}", graph.VertexCount));
      System.Diagnostics.Trace.WriteLine(string.Format("Edge count:\t{0}", graph.EdgeCount));
      System.Diagnostics.Trace.WriteLine(string.Format("Vertexes:"));
      foreach (Vertex v in graph.Vertexes)
        ShowVertex(v);
    }

    static void ShowVertex(Vertex head)
    {
      System.Diagnostics.Trace.Write(string.Format("  {0}", head.Name));
      AssemblyAbstraction A = head.Data as AssemblyAbstraction;
      if (A == null)
        System.Diagnostics.Trace.WriteLine("");
      else
        System.Diagnostics.Trace.WriteLine(string.Format(": Abstractness:{0:F2} Instability:{1:F2} DistanceToMainSequence:{2:F2}", A.Abstractness, A.Instability, A.DistanceToMainSequence));
      System.Diagnostics.Trace.WriteLine(string.Format("    Efferent couplings: {0}", head.OutgoingEdgeCount));
      foreach (Edge e in head.OutgoingEdges)
        System.Diagnostics.Trace.WriteLine(string.Format("\t{0}", e.Target.Name));
      System.Diagnostics.Trace.WriteLine(string.Format("    Afferent couplings: {0}",head.IncomingEdgeCount));
      foreach (Edge e in head.IncomingEdges)
        System.Diagnostics.Trace.WriteLine(string.Format("\t{0}", e.Source.Name));
    }

    static void WritingResults(DirectedGraph graph, Input input)
    {
      System.Diagnostics.Trace.Write("\n*** Writing results...");
      WriteExcel(graph, input.Excel);

      var visio = new DigraphAsVisioDrawingWriter();
      TextWriter writer = File.CreateText(input.Visio);
      visio.Write(writer, graph);

      var dgml = new DigraphAsDGMLWriter();
      TextWriter dgmlwriter = File.CreateText(input.DGML);
      dgml.Write(dgmlwriter, graph);

      System.Diagnostics.Trace.WriteLine("Done. ***\n");
    }

    static void WriteExcel(DirectedGraph graph,string filename)
    {
      using (StreamWriter writer = File.CreateText(filename))
      {
        writer.WriteLine("{0},{1},{2},{3}", "Package", "Abstractness", "Instability", "DistanceToMainSequence");
        writer.WriteLine("{0},{1},{2},{3}", "I", 0, 1, 0);
        writer.WriteLine("{0},{1},{2},{3}", "A", 1, 0, 0);
        foreach (Vertex package in graph.Vertexes)
        {
          AssemblyAbstraction A = package.Data as AssemblyAbstraction;
          if (A == null) continue;
          writer.WriteLine("{0},{1},{2},{3}", package.Name, A.Abstractness, A.Instability, A.DistanceToMainSequence);
        }
      }
    }

    static void file_included(AssemblyName assembly, FileInfo file)
    {
      System.Diagnostics.Trace.WriteLine(string.Format("  File included: {0} [{1}]", file.FullName, assembly.FullName));
    }
    static void file_excluded(FileInfo file)
    {
      System.Diagnostics.Trace.WriteLine(string.Format("  File excluded: {0}", file.FullName));
    }
    static void dir_notfound(string folder)
    {
      System.Diagnostics.Trace.WriteLine(string.Format("Folder not found: {0}", folder));
    }
    static void ref_included(Package package)
    {
      if(package is AssemblyNamePackage)
        System.Diagnostics.Trace.WriteLine(string.Format("\tReference included: {0}", (package as AssemblyNamePackage).AssemblyName.FullName));
      else
        System.Diagnostics.Trace.WriteLine(string.Format("\tReference mapping included: {0} <- [{1}]", package.Name, (package as MappedPackage).LastMappedFullName));
    }
    static void ref_excluded(Package package)
    {
      if (package is AssemblyNamePackage)
        System.Diagnostics.Trace.WriteLine(string.Format("\tReference excluded: {0}", (package as AssemblyNamePackage).AssemblyName.FullName));
      else
        System.Diagnostics.Trace.WriteLine(string.Format("\tReference mapping excluded: {0} <- [{1}]", package.Name, (package as MappedPackage).LastMappedFullName));
    }
    static void package_mapped(MappedPackage mapped)
    {
      System.Diagnostics.Trace.WriteLine(string.Format("  Assembly mapped: {0} <- [{1}]", mapped.Name, mapped.LastMappedFullName));
    }
    static void notfound(FileInfo file, FileNotFoundException ex)
    {
      System.Diagnostics.Trace.WriteLine(string.Format("  File not found:{0}", file.FullName));
    }
    static void notload(FileInfo file, FileLoadException ex)
    {
      System.Diagnostics.Trace.WriteLine(string.Format("  Cannot load this file:{0}", file.FullName));
    }
    static void badimage(FileInfo file, BadImageFormatException ex)
    {
      System.Diagnostics.Trace.WriteLine(string.Format("  This file is not a .NET assembly:{0}", file.FullName));
    }
  }
}